Skip to content

[tvOS] Fix unpressable buttons - #4233

Merged
m-bert merged 9 commits into
mainfrom
@mbert/tvos-buttons
Jun 3, 2026
Merged

[tvOS] Fix unpressable buttons#4233
m-bert merged 9 commits into
mainfrom
@mbert/tvos-buttons

Conversation

@m-bert

@m-bert m-bert commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds support for our button components (*Button / Pressable / Touchable) on tvOS.

Fixes #4224

Test plan

Checked on standalone `tvOS` app:
import React, { useState } from 'react';
import {
  Button,
  Pressable as RNPressable,
  ScrollView,
  StyleSheet,
  Text,
  TouchableOpacity as RNTouchableOpacity,
  View,
} from 'react-native';
import {
  GestureHandlerRootView,
  Pressable as GHPressable,
  RectButton,
  Touchable,
} from 'react-native-gesture-handler';

type LogEntry = { id: number; text: string };
let nextId = 0;

export default function App() {
  const [log, setLog] = useState<LogEntry[]>([]);

  function addLog(text: string) {
    setLog(prev => [{ id: nextId++, text }, ...prev].slice(0, 8));
  }

  function handlers(name: string) {
    return {
      onFocus: () => addLog(`[${name}] onFocus`),
      onBlur: () => addLog(`[${name}] onBlur`),
      onPress: () => addLog(`[${name}] onPress`),
    };
  }

  return (
    <GestureHandlerRootView style={styles.root}>
      <ScrollView contentContainerStyle={styles.content}>
        <View style={styles.columns}>
          <View style={styles.column}>
            <Text style={styles.sectionLabel}>React Native</Text>

            <RNPressable
              focusable
              disabled={true}
              style={styles.button}
              onFocus={() => addLog('[RN Pressable] onFocus')}
              onBlur={() => addLog('[RN Pressable] onBlur')}
              onPress={() => addLog('[RN Pressable] onPress')}
            >
              <Text style={styles.buttonText}>Pressable</Text>
            </RNPressable>

            <View style={styles.button}>
              <Button
                title="Button"
                onPress={() => addLog('[RN Button] onPress')}
              />
            </View>

            <RNTouchableOpacity
              focusable
              style={styles.button}
              onFocus={() => addLog('[RN TouchableOpacity] onFocus')}
              onBlur={() => addLog('[RN TouchableOpacity] onBlur')}
              onPress={() => addLog('[RN TouchableOpacity] onPress')}
            >
              <Text style={styles.buttonText}>TouchableOpacity</Text>
            </RNTouchableOpacity>
          </View>

          <View style={styles.column}>
            <Text style={styles.sectionLabel}>Gesture Handler</Text>

            <Touchable
              defaultOpacity={0.3}
              activeOpacity={0.7}
              activeScale={1.3}
              style={styles.button}
              {...handlers('GH Touchable')}
            >
              <Text style={styles.buttonText}>Touchable</Text>
            </Touchable>

            <GHPressable
              focusable
              style={styles.button}
              {...handlers('GH Pressable')}
            >
              <Text style={styles.buttonText}>Pressable</Text>
            </GHPressable>

            <RectButton
              focusable
              style={styles.button}
              {...handlers('GH RectButton')}
            >
              <Text style={styles.buttonText}>RectButton</Text>
            </RectButton>
          </View>
        </View>

        <View style={styles.logBox}>
          <Text style={styles.logTitle}>Event log</Text>
          {log.length === 0 ? (
            <Text style={styles.logEmpty}>No events yet</Text>
          ) : (
            log.map(entry => (
              <Text key={entry.id} style={styles.logEntry}>
                {entry.text}
              </Text>
            ))
          )}
        </View>
      </ScrollView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  content: {
    padding: 24,
    gap: 16,
  },
  columns: {
    flexDirection: 'row',
    gap: 24,
  },
  column: {
    flex: 1,
    gap: 12,
    alignItems: 'center',
  },
  sectionLabel: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 4,
  },
  button: {
    width: '100%',
    height: 50,
    borderRadius: 8,
    backgroundColor: 'blue',
    alignItems: 'center',
    justifyContent: 'center',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 16,
  },
  logBox: {
    backgroundColor: '#1a1a1a',
    borderRadius: 8,
    padding: 12,
    minHeight: 180,
  },
  logTitle: {
    fontWeight: 'bold',
    marginBottom: 6,
    color: '#aaa',
    fontSize: 14,
  },
  logEmpty: {
    color: '#555',
    fontStyle: 'italic',
  },
  logEntry: {
    fontFamily: 'monospace',
    fontSize: 13,
    color: '#ddd',
    marginBottom: 3,
  },
});
Screen.Recording.2026-06-03.at.12.23.27.mov

Copilot AI review requested due to automatic review settings June 3, 2026 10:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds tvOS-specific support for RNGH button-like components so they can become focusable/selectable and correctly drive press callbacks/animations when activated via the tvOS focus engine (Fixes #4224).

Changes:

  • Introduces a shared getTVProps helper to translate focusable into isTVSelectable for button components that bypass RN’s <View> prop translation.
  • Updates v3 Pressable to handle tvOS “Select” activation by directly driving the press handlers with a synthetic, center-based event and skipping touch bounds checks.
  • Adds tvOS-only hooks on the Fabric button component view to emit press events and trigger press-in/out animations.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/react-native-gesture-handler/src/v3/components/Touchable/Touchable.tsx Applies tvOS focusability props to v3 Touchable’s underlying native button component.
packages/react-native-gesture-handler/src/v3/components/Pressable.tsx Implements tvOS-specific press lifecycle handling for focus-engine “Select” presses and applies tvOS props.
packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx Applies tvOS focusability props to legacy button components (BaseButton/RawButton).
packages/react-native-gesture-handler/src/components/utils.ts Adds shared tvOS prop translation helper (getTVProps).
packages/react-native-gesture-handler/src/components/Pressable/utils.ts Adds mockCenterPressableEvent for focus-driven presses without touch coordinates.
packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm Adds tvOS-only methods to emit press events and trigger press animations on the Fabric component view.
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h Exposes tvOS-only animation methods on RNGestureHandlerButton.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native-gesture-handler/src/components/utils.ts Outdated
@m-bert
m-bert requested a review from j-piasecki June 3, 2026 10:36
Comment thread packages/react-native-gesture-handler/src/components/Pressable/utils.ts Outdated
@m-bert
m-bert merged commit 665c288 into main Jun 3, 2026
6 checks passed
@m-bert
m-bert deleted the @mbert/tvos-buttons branch June 3, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v3.0.0 tvOS: Touchable onFocus/onBlur never fire

3 participants